/*
 * $Id: AbstractPainter.java 4082 2011-11-15 18:39:43Z kschaefe $
 *
 * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle,
 * Santa Clara, California 95054, U.S.A. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

package org.jdesktop.swingx.painter;

import java.awt.AlphaComposite;
import java.awt.Composite;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.awt.image.BufferedImageOp;
import java.lang.ref.SoftReference;

import org.jdesktop.beans.AbstractBean;
import org.jdesktop.swingx.util.GraphicsUtilities;

/**
 * <p>
 * A convenient base class from which concrete {@link Painter} implementations
 * may extend. It extends {@link org.jdesktop.beans.AbstractBean} as a
 * convenience for adding property change notification support. In addition,
 * <code>AbstractPainter</code> provides subclasses with the ability to
 * cacheable painting operations, configure the drawing surface with common
 * settings (such as antialiasing and interpolation), and toggle whether a
 * subclass paints or not via the <code>visibility</code> property.
 * </p>
 *
 * <p>
 * Subclasses of <code>AbstractPainter</code> generally need only override the
 * {@link #doPaint(Graphics2D, Object, int, int)} method. If a subclass requires
 * more control over whether caching is enabled, or for configuring the graphics
 * state, then it may override the appropriate protected methods to interpose
 * its own behavior.
 * </p>
 * 
 * <p>
 * For example, here is the doPaint method of a simple <code>Painter</code> that
 * paints an opaque rectangle:
 * 
 * <pre>
 * <code>
 *  public void doPaint(Graphics2D g, T obj, int width, int height) {
 *      g.setPaint(Color.BLUE);
 *      g.fillRect(0, 0, width, height);
 *  }
 * </code>
 * </pre>
 * 
 * </p>
 *
 * @author rbair
 */
@SuppressWarnings("nls")
public abstract class AbstractPainter<T> extends AbstractBean implements Painter<T> {
	/**
	 * An enum representing the possible interpolation values of Bicubic,
	 * Bilinear, and Nearest Neighbor. These map to the underlying
	 * RenderingHints, but are easier to use and serialization safe.
	 */
	public enum Interpolation {
		/**
		 * use bicubic interpolation
		 */
		Bicubic(RenderingHints.VALUE_INTERPOLATION_BICUBIC),
		/**
		 * use bilinear interpolation
		 */
		Bilinear(RenderingHints.VALUE_INTERPOLATION_BILINEAR),
		/**
		 * use nearest neighbor interpolation
		 */
		NearestNeighbor(RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);

		private Object value;

		Interpolation(Object value) {
			this.value = value;
		}

		private void configureGraphics(Graphics2D g) {
			g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, value);
		}
	}

	// --------------------------------------------------- Instance Variables
	/**
	 * The cached image, if shouldUseCache() returns true
	 */
	private transient SoftReference<BufferedImage> cachedImage;
	private boolean cacheCleared = true;
	private boolean cacheable = false;
	private boolean dirty = false;
	private BufferedImageOp[] filters = new BufferedImageOp[0];
	private boolean antialiasing = true;
	private Interpolation interpolation = Interpolation.NearestNeighbor;
	private boolean visible = true;
	private boolean inPaintContext;

	/**
	 * Creates a new instance of AbstractPainter.
	 */
	public AbstractPainter() {
	}

	/**
	 * Creates a new instance of AbstractPainter.
	 * 
	 * @param cacheable
	 *            indicates if this painter should be cacheable
	 */
	public AbstractPainter(boolean cacheable) {
		setCacheable(cacheable);
	}

	/**
	 * A defensive copy of the Effects to apply to the results of the
	 * AbstractPainter's painting operation. The array may be empty but it will
	 * never be null.
	 * 
	 * @return the array of filters applied to this painter
	 */
	public final BufferedImageOp[] getFilters() {
		BufferedImageOp[] results = new BufferedImageOp[filters.length];
		System.arraycopy(filters, 0, results, 0, results.length);
		return results;
	}

	/**
	 * <p>
	 * A convenience method for specifying the filters to use based on
	 * BufferedImageOps. These will each be individually wrapped by an
	 * ImageFilter and then setFilters(Effect... filters) will be called with
	 * the resulting array
	 * </p>
	 * 
	 * 
	 * @param effects
	 *            the BufferedImageOps to wrap as filters
	 */
	public void setFilters(BufferedImageOp... effects) {
		if (effects == null)
			effects = new BufferedImageOp[0];
		BufferedImageOp[] old = getFilters();
		this.filters = new BufferedImageOp[effects.length];
		System.arraycopy(effects, 0, this.filters, 0, this.filters.length);
		setDirty(true);
		firePropertyChange("filters", old, getFilters());
	}

	/**
	 * Returns if antialiasing is turned on or not. The default value is true.
	 * This is a bound property.
	 * 
	 * @return the current antialiasing setting
	 */
	public boolean isAntialiasing() {
		return antialiasing;
	}

	/**
	 * Sets the antialiasing setting. This is a bound property.
	 * 
	 * @param value
	 *            the new antialiasing setting
	 */
	public void setAntialiasing(boolean value) {
		boolean old = isAntialiasing();
		antialiasing = value;
		if (old != value)
			setDirty(true);
		firePropertyChange("antialiasing", old, isAntialiasing());
	}

	/**
	 * Gets the current interpolation setting. This property determines if
	 * interpolation will be used when drawing scaled images. @see
	 * java.awt.RenderingHints.KEY_INTERPOLATION.
	 * 
	 * @return the current interpolation setting
	 */
	public Interpolation getInterpolation() {
		return interpolation;
	}

	/**
	 * Sets a new value for the interpolation setting. This setting determines
	 * if interpolation should be used when drawing scaled images. @see
	 * java.awt.RenderingHints.KEY_INTERPOLATION.
	 * 
	 * @param value
	 *            the new interpolation setting
	 */
	public void setInterpolation(Interpolation value) {
		Object old = getInterpolation();
		this.interpolation = value == null ? Interpolation.NearestNeighbor : value;
		if (old != value)
			setDirty(true);
		firePropertyChange("interpolation", old, getInterpolation());
	}

	/**
	 * Gets the visible property. This controls if the painter should paint
	 * itself. It is true by default. Setting visible to false is good when you
	 * want to temporarily turn off a painter. An example of this is a painter
	 * that you only use when a button is highlighted.
	 *
	 * @return current value of visible property
	 */
	public boolean isVisible() {
		return this.visible;
	}

	/**
	 * <p>
	 * Sets the visible property. This controls if the painter should paint
	 * itself. It is true by default. Setting visible to false is good when you
	 * want to temporarily turn off a painter. An example of this is a painter
	 * that you only use when a button is highlighted.
	 * </p>
	 *
	 * @param visible
	 *            New value of visible property.
	 */
	public void setVisible(boolean visible) {
		boolean old = isVisible();
		this.visible = visible;
		if (old != visible)
			setDirty(true); // not the most efficient, but I must do this
							// otherwise a CompoundPainter
							// or other aggregate painter won't know that it is
							// now invalid
							// there might be a tricky solution but that is a
							// performance optimization
		firePropertyChange("visible", old, isVisible());
	}

	/**
	 * <p>
	 * Gets whether this <code>AbstractPainter</code> can be cached as an image.
	 * If caching is enabled, then it is the responsibility of the developer to
	 * invalidate the painter (via {@link #clearCache}) if external state has
	 * changed in such a way that the painter is invalidated and needs to be
	 * repainted.
	 * </p>
	 *
	 * @return whether this is cacheable
	 */
	public boolean isCacheable() {
		return cacheable;
	}

	/**
	 * <p>
	 * Sets whether this <code>AbstractPainter</code> can be cached as an image.
	 * If true, this is treated as a hint. That is, a cacheable may or may not
	 * be used. The {@link #shouldUseCache} method actually determines whether
	 * the cacheable is used. However, if false, then this is treated as an
	 * absolute value. That is, no cacheable will be used.
	 * </p>
	 *
	 * <p>
	 * If set to false, then #clearCache is called to free system resources.
	 * </p>
	 *
	 * @param cacheable
	 */
	public void setCacheable(boolean cacheable) {
		boolean old = isCacheable();
		this.cacheable = cacheable;
		firePropertyChange("cacheable", old, isCacheable());
		if (!isCacheable()) {
			clearCache();
		}
	}

	/**
	 * <p>
	 * Call this method to clear the cacheable. This may be called whether there
	 * is a cacheable being used or not. If cleared, on the next call to
	 * <code>paint</code>, the painting routines will be called.
	 * </p>
	 *
	 * <p>
	 * <strong>Subclasses</strong>If overridden in subclasses, you
	 * <strong>must</strong> call super.clearCache, or physical resources (such
	 * as an Image) may leak.
	 * </p>
	 */
	public void clearCache() {
		BufferedImage cache = cachedImage == null ? null : cachedImage.get();
		if (cache != null) {
			cache.flush();
		}
		cacheCleared = true;
		if (!isCacheable()) {
			cachedImage = null;
		}
	}

	/**
	 * Only made package private for testing. Don't call this method outside of
	 * this class! This is NOT a bound property
	 */
	boolean isCacheCleared() {
		return cacheCleared;
	}

	/**
	 * <p>
	 * Called to allow <code>Painter</code> subclasses a chance to see if any
	 * state in the given object has changed from the last paint operation. If
	 * it has, then the <code>Painter</code> has a chance to mark itself as
	 * dirty, thus causing a repaint, even if cached.
	 * </p>
	 *
	 * @param object
	 */
	protected void validate(T object) {
	}

	/**
	 * Ye olde dirty bit. If true, then the painter is considered dirty and in
	 * need of being repainted. This is a bound property.
	 *
	 * @return true if the painter state has changed and the painter needs to be
	 *         repainted.
	 */
	protected boolean isDirty() {
		return dirty;
	}

	/**
	 * Sets the dirty bit. If true, then the painter is considered dirty, and
	 * the cache will be cleared. This property is bound.
	 *
	 * @param d
	 *            whether this <code>Painter</code> is dirty.
	 */
	protected void setDirty(boolean d) {
		boolean old = isDirty();
		this.dirty = d;
		firePropertyChange("dirty", old, isDirty());
		if (isDirty()) {
			clearCache();
		}
	}

	boolean isInPaintContext() {
		return inPaintContext;
	}

	void setInPaintContext(boolean inPaintContext) {
		this.inPaintContext = inPaintContext;
	}

	/**
	 * <p>
	 * Returns true if the painter should use caching. This method allows
	 * subclasses to specify the heuristics regarding whether to cache or not.
	 * If a <code>Painter</code> has intelligent rules regarding painting times,
	 * and can more accurately indicate whether it should be cached, it could
	 * implement that logic in this method.
	 * </p>
	 *
	 * @return whether or not a cache should be used
	 */
	protected boolean shouldUseCache() {
		return isCacheable() || filters.length > 0; // NOTE, I can only do this
													// because getFilters() is
													// final
	}

	/**
	 * <p>
	 * This method is called by the <code>paint</code> method prior to any
	 * drawing operations to configure the drawing surface. The default
	 * implementation sets the rendering hints that have been specified for this
	 * <code>AbstractPainter</code>.
	 * </p>
	 *
	 * <p>
	 * This method can be overridden by subclasses to modify the drawing surface
	 * before any painting happens.
	 * </p>
	 *
	 * @param g
	 *            the graphics surface to configure. This will never be null.
	 * @see #paint(Graphics2D, Object, int, int)
	 */
	protected void configureGraphics(Graphics2D g) {
		// configure antialiasing
		if (isAntialiasing()) {
			g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
		} else {
			g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
		}

		getInterpolation().configureGraphics(g);
	}

	/**
	 * Subclasses must implement this method and perform custom painting
	 * operations here.
	 * 
	 * @param width
	 * @param height
	 * @param g
	 *            The Graphics2D object in which to paint
	 * @param object
	 */
	protected abstract void doPaint(Graphics2D g, T object, int width, int height);

	/**
	 * @inheritDoc
	 */
	@Override
	public final void paint(Graphics2D g, T obj, int width, int height) {
		if (g == null) {
			throw new NullPointerException("The Graphics2D must be supplied");
		}

		if (!isVisible() || width < 1 || height < 1) {
			return;
		}

		configureGraphics(g);

		// paint to a temporary image if I'm caching, or if there are filters to
		// apply
		if (shouldUseCache() || filters.length > 0) {
			validate(obj);
			BufferedImage cache = cachedImage == null ? null : cachedImage.get();
			boolean invalidCache = null == cache || cache.getWidth() != width || cache.getHeight() != height;

			if (cacheCleared || invalidCache || isDirty()) {
				// rebuild the cacheable. I do this both if a cacheable is
				// needed, and if any
				// filters exist. I only *save* the resulting image if caching
				// is turned on
				if (invalidCache) {
					cache = GraphicsUtilities.createCompatibleTranslucentImage(width, height);
				}
				Graphics2D gfx = cache.createGraphics();

				try {
					gfx.setClip(0, 0, width, height);

					if (!invalidCache) {
						// If we are doing a repaint, but we didn't have to
						// recreate the image, we need to clear it back
						// to a fully transparent background.
						Composite composite = gfx.getComposite();
						gfx.setComposite(AlphaComposite.Clear);
						gfx.fillRect(0, 0, width, height);
						gfx.setComposite(composite);
					}

					configureGraphics(gfx);
					doPaint(gfx, obj, width, height);
				} finally {
					gfx.dispose();
				}

				if (!isInPaintContext()) {
					for (BufferedImageOp f : getFilters()) {
						cache = f.filter(cache, null);
					}
				}

				// only save the temporary image as the cacheable if I'm caching
				if (shouldUseCache()) {
					cachedImage = new SoftReference<BufferedImage>(cache);
					cacheCleared = false;
				}
			}

			g.drawImage(cache, 0, 0, null);
		} else {
			// can't use the cacheable, so just paint
			doPaint(g, obj, width, height);
		}

		// painting has occured, so restore the dirty bit to false
		setDirty(false);
	}
}
